本篇文章搬家囉! 這裡不再回覆留言,請移至 https://blog.jerry-hong.com/series/rxjs/thirty-days-RxJS-12/
今天要繼續講兩個簡單的 transformation operators 並帶一些小範例,這兩個 operators 都是實務上很常會用到的方法。
scan 其實就是 Observable 版本的 reduce 只是命名不同。如果熟悉陣列操作的話,應該會知道原生的 JS Array 就有 reduce 的方法,使用方式如下
var arr = [1, 2, 3, 4];
var result = arr.reduce((origin, next) => {
console.log(origin)
return origin + next
}, 0);
console.log(result)
// 0
// 1
// 3
// 6
// 10
reduce 方法需要傳兩個參數,第一個是 callback 第二個則是起始狀態,這個 callback 執行時,會傳入兩個參數一個是原本的狀態,第二個是修改原本狀態的參數,最後回傳一個新的狀態,再繼續執行。
所以這段程式碼是這樣執行的
scan 整體的運作方式都跟 reduce 一樣,範例如下
var source = Rx.Observable.from('hello')
.zip(Rx.Observable.interval(600), (x, y) => x);
var example = source.scan((origin, next) => origin + next, '');
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// h
// he
// hel
// hell
// hello
// complete
畫成 Marble Diagram
source : ----h----e----l----l----o|
scan((origin, next) => origin + next, '')
example: ----h----(he)----(hel)----(hell)----(hello)|
這裡可以看到第一次傳入 'h'
跟 ''
相加,返回 'h'
當作下一次的初始狀態,一直重複下去。
scan 跟 reduce 最大的差別就在 scan 一定會回傳一個 observable 實例,而 reduce 最後回傳的值有可能是任何資料型別,必須看使用者傳入的 callback 才能決定 reduce 最後的返回值。
Jafar Husain 就曾說:「JavaScript 的 reduce 是錯了,它最後應該永遠回傳陣列才對!」
如果大家之前有到這裡練習的話,會發現 reduce 被設計成一定回傳陣列,而這個網頁就是 Jafar 做的。
scan 很常用在狀態的計算處理,最簡單的就是對一個數字的加減,我們可以綁定一個 button 的 click 事件,並用 map 把 click event 轉成 1,之後送處 scan 計算值再做顯示。
這裡筆者寫了一個小範例,來示範如何做最簡單的加減
const addButton = document.getElementById('addButton');
const minusButton = document.getElementById('minusButton');
const state = document.getElementById('state');
const addClick = Rx.Observable.fromEvent(addButton, 'click').mapTo(1);
const minusClick = Rx.Observable.fromEvent(minusButton, 'click').mapTo(-1);
const numberState = Rx.Observable.empty()
.startWith(0)
.merge(addClick, minusClick)
.scan((origin, next) => origin + next, 0)
numberState
.subscribe({
next: (value) => { state.innerHTML = value;},
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
這裡我們用了兩個 button,一個是 add 按鈕,一個是 minus 按鈕。
我把這兩個按鈕的點擊事件各建立了 addClcik
, minusClick
兩個 observable,這兩個 observable 直接 mapTo(1) 跟 mapTo(-1),代表被點擊後會各自送出的數字!
接著我們用了 empty()
建立一個空的 observable 代表畫面上數字的狀態,搭配 startWith(0)
來設定初始值,接著用 merge
把兩個 observable 合併透過 scan 處理之後的邏輯,最後在 subscribe 來更改畫面的顯示。
這個小範例用到了我們這幾天講的 operators,包含 mapTo, empty, startWith, merge 還有現在講的 scan,建議讀者一定要花時間稍微練習一下。
buffer 是一整個家族,總共有五個相關的 operators
這裡比較常用到的是 buffer, bufferCount 跟 bufferTime 這三個,我們直接來看範例。
var source = Rx.Observable.interval(300);
var source2 = Rx.Observable.interval(1000);
var example = source.buffer(source2);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// [0,1,2]
// [3,4,5]
// [6,7,8]...
畫成 Marble Diagram 則像是
source : --0--1--2--3--4--5--6--7..
source2: ---------0---------1--------...
buffer(source2)
example: ---------([0,1,2])---------([3,4,5])
buffer 要傳入一個 observable(source2),它會把原本的 observable (source)送出的元素緩存在陣列中,等到傳入的 observable(source2) 送出元素時,就會觸發把緩存的元素送出。
這裡的範例 source2 是每一秒就會送出一個元素,我們可以改用 bufferTime 簡潔的表達,如下
var source = Rx.Observable.interval(300);
var example = source.bufferTime(1000);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// [0,1,2]
// [3,4,5]
// [6,7,8]...
除了用時間來作緩存外,我們更常用數量來做緩存,範例如下
var source = Rx.Observable.interval(300);
var example = source.bufferCount(3);
example.subscribe({
next: (value) => { console.log(value); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
// [0,1,2]
// [3,4,5]
// [6,7,8]...
JSBin | JSFiddle
在實務上,我們可以用 buffer 來做某個事件的過濾,例如像是滑鼠連點才能真的執行,這裡我們一樣寫了一個小範例
const button = document.getElementById('demo');
const click = Rx.Observable.fromEvent(button, 'click')
const example = click
.bufferTime(500)
.filter(arr => arr.length >= 2);
example.subscribe({
next: (value) => { console.log('success'); },
error: (err) => { console.log('Error: ' + err); },
complete: () => { console.log('complete'); }
});
這裡我們只有在 500 毫秒內連點兩下,才能成功印出 'success'
,這個功能在某些特殊的需求中非常的好用,也能用在批次處理來降低 request 傳送的次數!
今天我們介紹了兩個 operators 分別是 scan, buffer,也做了兩個小範例,不知道讀者有沒有收穫呢?
.scan()
跟原生的 .reduce()
的差異是在 .reduce()
回傳後不是一個observable 所以不能subscribe ; 相反, .scan()
就可
這樣理解是正確的嗎?
沒錯! Jafar 曾說過,他認為 JavaScript 錯了,reduce 應該固定回傳陣列才對。 XD
等等會補充到文章中,感謝你
bufferTime 很實用~
不過不知道要怎麼讓事件發生後再開始緩存呢
因為他按照自己的節奏送出值
所以有時候點兩下 剛好跨過 500 的區間
變成送出兩次 @@"
剛剛看到第14篇
用 click.throttleTime(500) 似乎有效XD!?
早安~
這個需求可以用 bufferWhen + delay 實現喔!
const testBtn = document.getElementById('test');
const click = Rx.Observable.fromEvent(testBtn, 'click');
click.bufferWhen(() => click.delay(500))
.subscribe(arr => {
console.log('當點擊後 500 毫秒內按了幾下: ' + arr.length)
})
關於 scan 用法
.scan((origin, next) => origin + next, 0)
給定初始值0,可能是作者的習慣。不過其實不需要給定,如次可以少一次迴圈。
假設 source: [1, 2, 3, 4]
有給0時,第一次迴圈的變數 origin: 0
, next:1
。然而沒給初始值時,函式會自動拿前兩個數值當 origin 和 next,第一次迴圈的變數 origin: 1
next: 2
。
可以把這兩段 reduce 語法貼到 console 比較看看
[1,2].reduce((p, n) => { console.log('loop'); return n + p; }, 0)
// loop
// loop
[1,2].reduce((p, n) => { console.log('loop'); return n + p; })
// loop (only loop once)
雖然是 recude,但是 scan 應該是一樣結果
恩恩 不一定要給,這裡是示範 API 的參數,忘了說不一定要給
請問一下,爲什麽要以Rx.Observable.empty().startWith(0)開始,empty以後就是complete,爲何後面的事件還能繼續?(merge())
我試了將empty換成throw就不行。從個人角度來看,Rx.Observable.of(0)才能理解。
Rx.Observable.empty().startWith(0) 就是 Rx.Observable.of(0) 喔
Rx.Observable.empty().startWith(0) 就是 Rx.Observable.of(0) 喔
不太明白buffercount 的功能。
是不是source1 每出三個元素就要打印出來?
對 每出三個元素 就往下送
本篇文章搬家囉! 這裡不再回覆留言,請移至 https://blog.jerry-hong.com/series/rxjs/thirty-days-RxJS-12/